//@version=5
indicator('RSI Divergence Projection', overlay=true, max_labels_count=500)

// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © wugamlo

// =========================================================================================
// INPUTS
// =========================================================================================

// --- RSI Inputs ---
rsiGroup = 'RSI Divergence'
len = input.int(title='RSI Period', minval=1, defval=14, group=rsiGroup, tooltip='The number of bars used to calculate the RSI. The standard value is 14.')
src = close

// --- Divergence Inputs ---
divsrc = input.string(title='Use Price Wicks or Bodies?', defval='Bodies', options=['Wicks', 'Bodies'], group=rsiGroup, tooltip="Determines whether to use candle wicks (high/low) or candle bodies (max/min of open/close) for detecting price pivots. 'Wicks' is more sensitive to volatility.")
lbL = input.int(title='Pivot Lookback Left', defval=5, inline='Direction', group=rsiGroup, tooltip='The number of bars to the left and right of a pivot. A higher Left value finds more significant, longer-term pivots. A pivot is only confirmed after Right bars have passed.')
lbR = input.int(title='Right', defval=3, inline='Direction', group=rsiGroup)
rangeUpper = input.int(title='Lookback Range Max', defval=60, inline='Range', group=rsiGroup, tooltip='The maximum and minimum number of bars allowed between two pivots for a divergence to be considered valid.')
rangeLower = input.int(title='Min', defval=5, inline='Range', group=rsiGroup)
plotBull = input.bool(title='Plot Bullish', defval=true, inline='Bullish', group=rsiGroup, tooltip='Toggles the display of Confirmed Regular (first box) and Hidden (second box) Bullish divergences.')
plotHiddenBull = input.bool(title='Hidden', defval=false, inline='Bullish', group=rsiGroup)
plotBear = input.bool(title='Plot Bearish', defval=true, inline='Bearish', group=rsiGroup, tooltip='Toggles the display of Confirmed Regular (first box) and Hidden (second box) Bearish divergences.')
plotHiddenBear = input.bool(title='Hidden', defval=false, inline='Bearish', group=rsiGroup)

// --- Predictive Inputs ---
predictiveGroup = 'Predictive Divergences'
showPredictive = input.bool(true, 'Show Predictive Divergences', group=predictiveGroup, tooltip='Master switch to enable or disable all real-time predictive divergence analysis on the current (live) bar.')
potentialMinBars = input.int(5, 'Min Bars Since Last Pivot for Predictive', group=predictiveGroup, tooltip='The minimum number of bars that must pass since the last pivot before the script starts looking for a new potential divergence.')
showThreshold = input.bool(true, 'Show Divergence Confirmation Price', group=predictiveGroup, tooltip='Toggles the display of the projection line and label for the Confirmation Price on the live bar.')

// --- Alert Inputs ---
alertGroup = 'Alerts'
alertOnPotentialBull = input.bool(true, "Alert on Potential Bullish", group=alertGroup, inline="PotBull", tooltip="Triggers an alert when a potential divergence is first detected. Use the first box for Regular Bullish and the second for Hidden Bullish. To use, create one alert on the chart and set its condition to 'Any alert() function call'.")
alertOnPotentialHiddenBull = input.bool(false, "Hidden", group=alertGroup, inline="PotBull")
alertOnPotentialBear = input.bool(true, "Alert on Potential Bearish", group=alertGroup, inline="PotBear", tooltip="Triggers an alert when a potential divergence is first detected. Use the first box for Regular Bearish and the second for Hidden Bearish.")
alertOnPotentialHiddenBear = input.bool(false, "Hidden", group=alertGroup, inline="PotBear")

// --- Divergence Strength Score Inputs ---
strengthGroup = 'Divergence Strength Score'
showStrength = input.bool(true, 'Show Strength Score', group=strengthGroup, tooltip='If checked, the score (out of 10) will be displayed on the real-time projection label and included in alerts.')
rsiOB_L1 = input.int(70, 'RSI Overbought Level 1', group=strengthGroup, inline="OB", tooltip='Defines the RSI levels for overbought conditions, used to calculate the Location Score. Level 2 is for more extreme conditions and results in a higher score.')
rsiOB_L2 = input.int(80, 'Level 2', group=strengthGroup, inline="OB")
rsiOS_L1 = input.int(30, 'RSI Oversold Level 1', group=strengthGroup, inline="OS", tooltip='Defines the RSI levels for oversold conditions, used to calculate the Location Score. Level 2 is for more extreme conditions and results in a higher score.')
rsiOS_L2 = input.int(20, 'Level 2', group=strengthGroup, inline="OS")

// --- Plot Colors ---
bearColor = #DE071C
bullColor = #0CAB07
hiddenBullColor = #8BDE7A
hiddenBearColor = #DEA7A6
textColor = color.white
noneColor = color.new(color.white, 100)

// =========================================================================================
// CALCULATIONS
// =========================================================================================

osc = ta.rsi(src, len)

plFound = na(ta.pivotlow(osc, lbL, lbR)) ? false : true
phFound = na(ta.pivothigh(osc, lbL, lbR)) ? false : true

_inRange(cond) =>
    bars = ta.barssince(cond == true)
    rangeLower <= bars and bars <= rangeUpper

srch = divsrc == 'Wicks' ? high : math.max(close, open)
srcl = divsrc == 'Wicks' ? low : math.min(close, open)

// =========================================================================================
// CONFIRMED DIVERGENCE PLOTTING
// =========================================================================================

oscHL = osc[lbR] > ta.valuewhen(plFound, osc[lbR], 1) and _inRange(plFound[1])
priceLL = srcl[lbR] < ta.valuewhen(plFound, srcl[lbR], 1)
bullCond = plotBull and priceLL and oscHL and plFound
plot(plFound ? srcl[lbR] : na, offset=-lbR, title='Regular Bullish', linewidth=2, color=bullCond ? bullColor : noneColor)
plotshape(bullCond ? srcl[lbR] : na, offset=-lbR, title='Regular Bullish Label', text=' B ', style=shape.labelup, location=location.absolute, color=color.new(bullColor, 50), textcolor=color.new(textColor, 50))

oscLL = osc[lbR] < ta.valuewhen(plFound, osc[lbR], 1) and _inRange(plFound[1])
priceHL = srcl[lbR] > ta.valuewhen(plFound, srcl[lbR], 1)
hiddenBullCond = plotHiddenBull and priceHL and oscLL and plFound
plot(plFound ? srcl[lbR] : na, offset=-lbR, title='Hidden Bullish', linewidth=2, color=hiddenBullCond ? hiddenBullColor : noneColor)
plotshape(hiddenBullCond ? srcl[lbR] : na, offset=-lbR, title='Hidden Bullish Label', text=' HB ', style=shape.labelup, location=location.absolute, color=color.new(bullColor, 50), textcolor=color.new(textColor, 50))

oscLH = osc[lbR] < ta.valuewhen(phFound, osc[lbR], 1) and _inRange(phFound[1])
priceHH = srch[lbR] > ta.valuewhen(phFound, srch[lbR], 1)
bearCond = plotBear and priceHH and oscLH and phFound
plot(phFound ? srch[lbR] : na, offset=-lbR, title='Regular Bearish', linewidth=2, color=bearCond ? bearColor : noneColor)
plotshape(bearCond ? srch[lbR] : na, offset=-lbR, title='Regular Bearish Label', text=' B ', style=shape.labeldown, location=location.absolute, color=color.new(bearColor, 50), textcolor=color.new(textColor, 50))

oscHH = osc[lbR] > ta.valuewhen(phFound, osc[lbR], 1) and _inRange(phFound[1])
priceLH = srch[lbR] < ta.valuewhen(phFound, srch[lbR], 1)
hiddenBearCond = plotHiddenBear and priceLH and oscHH and phFound
plot(phFound ? srch[lbR] : na, offset=-lbR, title='Hidden Bearish', linewidth=2, color=hiddenBearCond ? hiddenBearColor : noneColor)
plotshape(hiddenBearCond ? srch[lbR] : na, offset=-lbR, title='Hidden Bearish Label', text=' HB ', style=shape.labeldown, location=location.absolute, color=color.new(bearColor, 50), textcolor=color.new(textColor, 50))

// =========================================================================================
// PREDICTIVE LOGIC & FUNCTIONS
// =========================================================================================

change = ta.change(close)
gain = change > 0 ? change : 0
loss = change < 0 ? -change : 0
avg_gain = ta.rma(gain, len)
avg_loss = ta.rma(loss, len)

var float lastPriceLow = na, var float lastRsiLow = na, var int lastLowBar = na
var float lastPriceHigh = na, var float lastRsiHigh = na, var int lastHighBar = na

if plFound
    lastPriceLow := srcl[lbR], lastRsiLow := osc[lbR], lastLowBar := bar_index - lbR
if phFound
    lastPriceHigh := srch[lbR], lastRsiHigh := osc[lbR], lastHighBar := bar_index - lbR

// --- Multi-line function for stability ---
f_threshold_close(rsi_target, prev_avg_gain, prev_avg_loss, _len, prev_close) =>
    if rsi_target >= 100 or rsi_target <= 0
        na
    else
        rs_target = rsi_target / (100.0 - rsi_target)
        required_avg_gain_g = rs_target * (prev_avg_loss * (_len - 1.0)) / _len
        required_gain = (required_avg_gain_g * _len) - (prev_avg_gain * (_len - 1.0))
        close_if_gain = required_gain > 0 ? prev_close + required_gain : na
        required_avg_loss_l = ((prev_avg_gain * (_len - 1.0)) / _len) / rs_target
        required_loss = (required_avg_loss_l * _len) - (prev_avg_loss * (_len - 1.0))
        close_if_loss = required_loss > 0 ? prev_close - required_loss : na
        float rsi_if_gain = na
        if not na(close_if_gain)
            change_g = close_if_gain - prev_close, gain_g = change_g > 0 ? change_g : 0, loss_g = change_g < 0 ? -change_g : 0
            avg_gain_g = (prev_avg_gain * (_len - 1) + gain_g) / _len, avg_loss_g = (prev_avg_loss * (_len - 1) + loss_g) / _len
            rsi_if_gain := 100 - (100 / (1 + (avg_gain_g / avg_loss_g)))
        float rsi_if_loss = na
        if not na(close_if_loss)
            change_l = close_if_loss - prev_close, gain_l = change_l > 0 ? change_l : 0, loss_l = change_l < 0 ? -change_l : 0
            avg_gain_l = (prev_avg_gain * (_len - 1) + gain_l) / _len, avg_loss_l = (prev_avg_loss * (_len - 1) + loss_l) / _len
            rsi_if_loss := 100 - (100 / (1 + (avg_gain_l / avg_loss_l)))
        diff_g = math.abs(rsi_if_gain - rsi_target), diff_l = math.abs(rsi_if_loss - rsi_target)
        na(diff_g) and na(diff_l) ? na : na(diff_g) ? close_if_loss : na(diff_l) ? close_if_gain : diff_g < diff_l ? close_if_gain : close_if_loss

// --- Expanded Numerical Strength Score Function ---
f_getStrengthScore(type, rsi_p1, rsi_p2, price_p1, price_p2) =>
    // 1. Location Score (1-5 points)
    int location_score = 1
    if type == 'bull'
        if rsi_p1 < rsiOS_L2 and rsi_p2 < rsiOS_L2
            location_score := 5 // Both pivots deeply oversold
        else if rsi_p1 < rsiOS_L1 and rsi_p2 < rsiOS_L1
            location_score := 4 // Both pivots oversold
        else if rsi_p1 < rsiOS_L1 or rsi_p2 < rsiOS_L1
            location_score := 3 // One pivot oversold
        else if rsi_p1 < 40 or rsi_p2 < 40
            location_score := 2 // Nearing oversold
    else if type == 'bear'
        if rsi_p1 > rsiOB_L2 and rsi_p2 > rsiOB_L2
            location_score := 5 // Both pivots deeply overbought
        else if rsi_p1 > rsiOB_L1 and rsi_p2 > rsiOB_L1
            location_score := 4 // Both pivots overbought
        else if rsi_p1 > rsiOB_L1 or rsi_p2 > rsiOB_L1
            location_score := 3 // One pivot overbought
        else if rsi_p1 > 60 or rsi_p2 > 60
            location_score := 2 // Nearing overbought
    else // Hidden divergences
        location_score := 3 // Neutral score for continuation patterns

    // 2. Magnitude Score (1-5 points)
    float price_change_pct = price_p1 != 0 ? ((price_p2 / price_p1) - 1) * 100 : 0.0
    float rsi_change_pct = rsi_p1 != 0 ? ((rsi_p2 / rsi_p1) - 1) * 100 : 0.0
    disagreement_score = math.abs(rsi_change_pct - price_change_pct)
    
    int magnitude_score = 1
    if disagreement_score > 100
        magnitude_score := 5 // Extreme disagreement
    else if disagreement_score > 75
        magnitude_score := 4
    else if disagreement_score > 50
        magnitude_score := 3
    else if disagreement_score > 25
        magnitude_score := 2

    // 3. Return combined score
    total_score = location_score + magnitude_score
    total_score

// --- Predictive divergence trend lines ---
var line bullLine = na, var line hiddenBullLine = na, var line bearLine = na, var line hiddenBearLine = na

// --- Management Variables for Projection Lines and Labels ---
var line bullThreshLine = na, var label bullInfoLabel = na
var line hiddenBullThreshLine = na, var label hiddenBullInfoLabel = na
var line bearThreshLine = na, var label bearInfoLabel = na
var line hiddenBearThreshLine = na, var label hiddenBearInfoLabel = na

// --- State management variables for alerts ---
var bool isBullAlertSent = false
var bool isHiddenBullAlertSent = false
var bool isBearAlertSent = false
var bool isHiddenBearAlertSent = false

// =========================================================================================
// REAL-TIME / LAST BAR LOGIC
// =========================================================================================

if showPredictive and barstate.islast
    // --- Delete Old Drawings ---
    line.delete(bullThreshLine), label.delete(bullInfoLabel)
    line.delete(hiddenBullThreshLine), label.delete(hiddenBullInfoLabel)
    line.delete(bearThreshLine), label.delete(bearInfoLabel)
    line.delete(hiddenBearThreshLine), label.delete(hiddenBearInfoLabel)

    // --- Check for Potential Divergences ---
    bool potentialBull = not na(lastPriceLow) and bar_index - lastLowBar >= potentialMinBars and srcl < lastPriceLow and osc > lastRsiLow
    bool potentialHiddenBull = not na(lastPriceLow) and bar_index - lastLowBar >= potentialMinBars and srcl > lastPriceLow and osc < lastRsiLow
    bool potentialBear = not na(lastPriceHigh) and bar_index - lastHighBar >= potentialMinBars and srch > lastPriceHigh and osc < lastRsiHigh
    bool potentialHiddenBear = not na(lastPriceHigh) and bar_index - lastHighBar >= potentialMinBars and srch < lastPriceHigh and osc > lastRsiHigh

    // --- Draw Price Divergence Dashed Lines ---
    if potentialBull and plotBull
        if na(bullLine)
            bullLine := line.new(lastLowBar, lastPriceLow, bar_index, srcl, color=color.green, style=line.style_dashed)
        else
            line.set_xy1(bullLine, lastLowBar, lastPriceLow), line.set_xy2(bullLine, bar_index, srcl)
    else if not na(bullLine)
        line.delete(bullLine), bullLine := na

    if potentialHiddenBull and plotHiddenBull
        if na(hiddenBullLine)
            hiddenBullLine := line.new(lastLowBar, lastPriceLow, bar_index, srcl, color=color.lime, style=line.style_dashed)
        else
            line.set_xy1(hiddenBullLine, lastLowBar, lastPriceLow), line.set_xy2(hiddenBullLine, bar_index, srcl)
    else if not na(hiddenBullLine)
        line.delete(hiddenBullLine), hiddenBullLine := na

    if potentialBear and plotBear
        if na(bearLine)
            bearLine := line.new(lastHighBar, lastPriceHigh, bar_index, srch, color=color.red, style=line.style_dashed)
        else
            line.set_xy1(bearLine, lastHighBar, lastPriceHigh), line.set_xy2(bearLine, bar_index, srch)
    else if not na(bearLine)
        line.delete(bearLine), bearLine := na

    if potentialHiddenBear and plotHiddenBear
        if na(hiddenBearLine)
            hiddenBearLine := line.new(lastHighBar, lastPriceHigh, bar_index, srch, color=color.maroon, style=line.style_dashed)
        else
            line.set_xy1(hiddenBearLine, lastHighBar, lastPriceHigh), line.set_xy2(hiddenBearLine, bar_index, srch)
    else if not na(hiddenBearLine)
        line.delete(hiddenBearLine), hiddenBearLine := na

    // --- Confirmation Price Projection Logic with Strength Score ---
    if showThreshold
        int lineOffset = 10 

        // --- Potential Bullish ---
        if potentialBull and plotBull
            score = f_getStrengthScore('bull', lastRsiLow, osc, lastPriceLow, srcl)
            string strengthText = showStrength ? 'Score: ' + str.tostring(score) + '/10\n' : ''
            float confirmation_price = f_threshold_close(lastRsiLow, avg_gain[1], avg_loss[1], len, close[1])
            if not na(confirmation_price)
                bullThreshLine := line.new(bar_index, confirmation_price, bar_index + lineOffset, confirmation_price, color=color.new(bullColor, 20), style=line.style_dashed)
                bullInfoLabel := label.new(bar_index + lineOffset, confirmation_price, 'Potential Bull RSI Div\n' + strengthText + 'Confirm @: ' + str.tostring(confirmation_price, '#.##'), color=color.new(bullColor, 20), style=label.style_label_left, textcolor=textColor, textalign=text.align_left)
                // --- ALERT LOGIC ---
                if alertOnPotentialBull and not isBullAlertSent
                    string alertMsg = 'Potential Bullish RSI Divergence on ' + syminfo.ticker + ' (' + timeframe.period + ')\n' + (showStrength ? 'Score: ' + str.tostring(score) + '/10\n' : '') + 'Confirmation Price: ' + str.tostring(confirmation_price, '#.##')
                    alert(alertMsg, freq = alert.freq_once_per_bar)
                    isBullAlertSent := true

        // --- Potential Hidden Bullish ---
        if potentialHiddenBull and plotHiddenBull
            score = f_getStrengthScore('hiddenbull', lastRsiLow, osc, lastPriceLow, srcl)
            string strengthText = showStrength ? 'Score: ' + str.tostring(score) + '/10\n' : ''
            float confirmation_price = f_threshold_close(lastRsiLow, avg_gain[1], avg_loss[1], len, close[1])
            if not na(confirmation_price)
                hiddenBullThreshLine := line.new(bar_index, confirmation_price, bar_index + lineOffset, confirmation_price, color=color.new(hiddenBullColor, 20), style=line.style_dashed)
                hiddenBullInfoLabel := label.new(bar_index + lineOffset, confirmation_price, 'Potential HBull RSI Div\n' + strengthText + 'Confirm @: ' + str.tostring(confirmation_price, '#.##'), color=color.new(hiddenBullColor, 20), style=label.style_label_left, textcolor=textColor, textalign=text.align_left)
                // --- ALERT LOGIC ---
                if alertOnPotentialHiddenBull and not isHiddenBullAlertSent
                    string alertMsg = 'Potential Hidden Bullish RSI Divergence on ' + syminfo.ticker + ' (' + timeframe.period + ')\n' + (showStrength ? 'Score: ' + str.tostring(score) + '/10\n' : '') + 'Confirmation Price: ' + str.tostring(confirmation_price, '#.##')
                    alert(alertMsg, freq = alert.freq_once_per_bar)
                    isHiddenBullAlertSent := true

        // --- Potential Bearish ---
        if potentialBear and plotBear
            score = f_getStrengthScore('bear', lastRsiHigh, osc, lastPriceHigh, srch)
            string strengthText = showStrength ? 'Score: ' + str.tostring(score) + '/10\n' : ''
            float confirmation_price = f_threshold_close(lastRsiHigh, avg_gain[1], avg_loss[1], len, close[1])
            if not na(confirmation_price)
                bearThreshLine := line.new(bar_index, confirmation_price, bar_index + lineOffset, confirmation_price, color=color.new(bearColor, 20), style=line.style_dashed)
                bearInfoLabel := label.new(bar_index + lineOffset, confirmation_price, 'Potential Bear RSI Div\n' + strengthText + 'Confirm @: ' + str.tostring(confirmation_price, '#.##'), color=color.new(bearColor, 20), style=label.style_label_left, textcolor=textColor, textalign=text.align_left)
                // --- ALERT LOGIC ---
                if alertOnPotentialBear and not isBearAlertSent
                    string alertMsg = 'Potential Bearish RSI Divergence on ' + syminfo.ticker + ' (' + timeframe.period + ')\n' + (showStrength ? 'Score: ' + str.tostring(score) + '/10\n' : '') + 'Confirmation Price: ' + str.tostring(confirmation_price, '#.##')
                    alert(alertMsg, freq = alert.freq_once_per_bar)
                    isBearAlertSent := true

        // --- Potential Hidden Bearish ---
        if potentialHiddenBear and plotHiddenBear
            score = f_getStrengthScore('hiddenbear', lastRsiHigh, osc, lastPriceHigh, srch)
            string strengthText = showStrength ? 'Score: ' + str.tostring(score) + '/10\n' : ''
            float confirmation_price = f_threshold_close(lastRsiHigh, avg_gain[1], avg_loss[1], len, close[1])
            if not na(confirmation_price)
                hiddenBearThreshLine := line.new(bar_index, confirmation_price, bar_index + lineOffset, confirmation_price, color=color.new(hiddenBearColor, 20), style=line.style_dashed)
                hiddenBearInfoLabel := label.new(bar_index + lineOffset, confirmation_price, 'Potential HBear RSI Div\n' + strengthText + 'Confirm @: ' + str.tostring(confirmation_price, '#.##'), color=color.new(hiddenBearColor, 20), style=label.style_label_left, textcolor=textColor, textalign=text.align_left)
                // --- ALERT LOGIC ---
                if alertOnPotentialHiddenBear and not isHiddenBearAlertSent
                    string alertMsg = 'Potential Hidden Bearish RSI Divergence on ' + syminfo.ticker + ' (' + timeframe.period + ')\n' + (showStrength ? 'Score: ' + str.tostring(score) + '/10\n' : '') + 'Confirmation Price: ' + str.tostring(confirmation_price, '#.##')
                    alert(alertMsg, freq = alert.freq_once_per_bar)
                    isHiddenBearAlertSent := true

    // --- Reset alert flags if conditions are no longer met ---
    if not potentialBull
        isBullAlertSent := false
    if not potentialHiddenBull
        isHiddenBullAlertSent := false
    if not potentialBear
        isBearAlertSent := false
    if not potentialHiddenBear
        isHiddenBearAlertSent := false